Objectif : étudier l'impact des techniques de transformations de données sur une modélisation linéaire de la consommation d'énergie des bâtiments de Seattle. On va considérer trois transformations de données : la suppression des outliers, la standardisation des variables d'entrée, et le passage au logarithme de la variable cible. On variable cible modélisée dans cette étude est la consommation d'énergie annuelle des batîments de Seattles. Cette modélisation s'appuie sur les informations de taille des bâtiment (surface au plancher, nombre d'étage, etc). Les données ont été collectées par la ville de Seattle en 2016. On utilisera comme modèle une regression linéaire, qui a l'avantage d'être simple d'utilisation (pas d'hyperparamètre) et facile à interpréter. Le choix d'un modèle linaire est justifié pour cette problématique. En effet, dans une étude exploratoire antérieure, on a pu constater que la consommation d'énergie est bien approximativement proportionnelle à la taille du batîment.
Cette étude est divisée en trois parties, dont chacune montre l'impact d'une des trois transformation de données suivantes :
## ensembles des bibliothèque pour l'analyse
## traitement des données
import pandas as pd
import numpy as np
## visualisation
import plotly.graph_objs as go
import plotly.figure_factory as ff
import plotly.express as px
import ipywidgets as ipy
## modélisation
from sklearn import preprocessing ## pour standardisation
from sklearn.model_selection import train_test_split ## validation model
from sklearn import linear_model ## pour régression
from sklearn.metrics import * ## métriques d'évaluation
## mes fonctions
import myutility.myfunctionLin as myfunc
On charge le jeu de données préparé/nettoyé pour pouvoir effectuer une modélisation linéaire de la consommation d'énergie des bâtiments de Seatle. Le jeu de données contient une sélection de variables : la variable cible et les variables d'entrées numériques. Ces variables d'entrées ont un comportement linéaire avec la variable cible. On a également rejeté une vingtaines de batîments aux consommations d'énergies atypiques, car trop importantes.
Un échantillon de cinq batîments du jeu de données est présentée ci-dessous. Au total, le jeu de données considéré dans cette étude est composé de 3320 batîments, qui sont renseignés par 7 variables.
Parmi ces variables on retrouve la variable cible SiteEnergyUseWN(kBtu), la surface au plancher totale PropertyGFATotal ou encore le nombre d'étages NumberofFloors.
## chargement des données
data = pd.read_csv('dataset/2016-building-energy-benchmarking-for-linear-model.csv')
data.shape
## sous-échantillon de 5 bâtiments
data.head(5)
Échantillon du dataframe utilisé pour la modélisation de la consommation d'énergie.
La majorité des batîments de Seattles ont une consommation relativement faible, autour de 700k kBtu (mode de l'hisogramme ci-dessous). Cependant de nombreux batîments ont une forte consommation d'énergie, et la distribution de consommation d'énergie est très étalée vers les valeurs positives, comme on peut le voir sur le graphique ci-dessous. La consommation d'énergie médiane est d'environ 2M kBtu.
px.histogram(data, x = 'SiteEnergyUseWN(kBtu)', marginal="box", nbins= 500, opacity=0.8)
Distribution de la variable cible y : la consommation d'énergie annuelle corrigée.
Termes constants
tableHeader = ['Model', 'MSE', 'R2', 'MSLE', 'Overtraining']
myRState = 0 ## random state
myTSize = 0.3 ## taille du jeu de test (fraction du jeu total)
mycolors=px.colors.qualitative.Vivid ## couleur plots
## séparation des variables d'entrée X et variable cible y
X = data.iloc[:, :-1]
y = data['SiteEnergyUseWN(kBtu)']
## séparation entraînement-test
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=myTSize, random_state=myRState)
## index des outliers
outliers_train, outliers_test = myfunc.outliersTrainTest(y_train, y_test)
Les outliers sont définis comme les bâtiments ayant une consommation d'énergie nulle.
On réalise une première régression linéaire qui nous servira de référence (baseline) tout au long de l'étude. On applique pour cette baseline les transformations de données 'usuelles' avant l'ajustement (fit) de la régression linéaire. Ces transformations sont :
La suppression d'outliers consiste à supprimer les batîments qui ont une consommation d'énergie annuelle nulle. La standardisation des variables d'entrée est une transformation des variables d'entrée telles que leur distribution aie une moyenne nulle et une déviation standard unité. Enfin le passage au log consiste à considérer comme la varaible cible non pas la consommation d'énergie, mais son logarithme.
## suppression des outliers
X_train_woOut, X_test_woOut, y_train_woOut, y_test_woOut = myfunc.removeOutliersXy(
X_train, X_test, y_train, y_test, outliers_train, outliers_test)
## fit du scaler à partir du jeu d'entraînement sans outliers
scaler_train_woOut = preprocessing.StandardScaler().fit(X_train_woOut)
## application du scaler sur le jeu d'entraînement et de test
X_train_woOut_normed, X_test_woOut_normed = myfunc.applyScalerTrainTest(scaler_train_woOut,
X_train_woOut, X_test_woOut)
## passage de la variable cible au log
y_train_woOut_log, y_test_woOut_log = myfunc.applyLogYTrainTest(y_train_woOut, y_test_woOut)
Ensemble des transformations de données réalisées avant l'entraînement de la régression linéaire de référence.
Entraînement du modèle de référence
La régression linéaire de référence permet d'expliquer une partie de la variation de la consommation d'énergie, avec un R2 proche de 40%. Le sur-entraînement, défini comme le rapport $MSE_{test} / MSE_{train}$, est relativement faible (1.1).
## entraînement et prédictions de la rég. linéaire de référence
y_train_pred_lrRef, y_test_pred_lrRef = myfunc.linearRegTrainTestPred(
X_train_woOut_normed, X_test_woOut_normed, y_train_woOut_log)
## métriques d'évalution de la reg linéaire
metrics_lrRef = myfunc.eval_metric('Baseline', y_test_woOut_log, y_test_pred_lrRef,
y_train_woOut_log, y_train_pred_lrRef, 1)
## table de performances
fig = ff.create_table([tableHeader, metrics_lrRef], height_constant=60)
fig.show()
Table des mesures de performances pour la régression linéaire de référence.
La distribution de la variable cible prédite est présentée sur le graphique de gauche ci-dessous. Elle est comparée à la valeur vraie, dont la distribution est légèrement gaussienne. Par sa forme plus piquée, la distribution des prédictions indique que le modèle n'arrive pas à 'capturer' toute la variabilité de la consommation d'énergie. Par ailleurs, la plupart des valeurs prédites (mode) se trouvent autour de la moyenne de la valeur vraie.
Le passage au logarithme de la variable cible a permis de réaliser notre régression linéaire de référence. Pour continuer l'interprétation des résultats, on repasse les prédictions 'à la normale', par passage à l'exponentielle ( $exp ( log ) = Identite$ ). La distribution de la consommation d'énergie prédite est présentée ci-dessous sur le graphe de droite. On constate que le modèle a tendance à surestimer la consommation d'énergie pour les faibles consommations d'énergie. La queue de distribution de la prédiction est plus petite que celle des valeurs vraies, et indique que les hautes consommations sont également sous-estimées.
## prédiction et valeur vraie de y
df_truePred_lrRef = myfunc.yTruePred(y_test_woOut_log, y_test_pred_lrRef, error = False, relativeError = False)
## on repasse y 'Ã la normale', par l'expenentielle
df_truePred_lrRef_exp = myfunc.yTruePred(np.exp(y_test_woOut_log)-1, np.exp(y_test_pred_lrRef)-1)
## supprime les valeurs prédites aberrantes (ie > 1B)
indexOut = df_truePred_lrRef_exp[df_truePred_lrRef_exp['pred'] > 1e9].index
df_truePred_lrRef_exp.drop(indexOut,inplace=True)
fig1 = px.histogram(df_truePred_lrRef, x = ['true', 'pred'], barmode='overlay')
fig1.update_layout(xaxis_title='Log of SiteEnergyUseWN(kBtu)', width= 480)
fig2 = px.histogram(df_truePred_lrRef_exp, x = ['true', 'pred'],
range_x = [0, 20e6], nbins = 2000, barmode='overlay')
fig2.update_layout(xaxis_title='SiteEnergyUseWN(kBtu)', width= 480)
ipy.HBox([go.FigureWidget(fig1), go.FigureWidget(fig2)])
Distribution de la consommation d'énergie annuelle : à gauche son logarithme, à droite elle-même (identité). La distribution de la valeur vraie (bleue) est comparée à la valeur prédite par le modèle (rouge). Le modèle est la régression linéaire de référence.
Pour chaque batîment, l'erreur de la prédiction est définie comme la différence entre sa prédiction de la consommation d'énergie et sa vraie valeur. La distribution de cette erreur en fonction de la consommation d'énergie vraie est présentée ci-dessous.
On observe que le modèle a tendance à surestimer la consommation d'énergie pour les batîments qui consomment peu (environ moins que 1,5M kBtu). Pour des commations supérieures, cette tendance s'inverse et le modèle a plus tendance à sous-estimer la consomation d'énergie. Malgré des sous-estimations plus fréquentes pour les bâtiments aux hautes consommations, les surestimations sont bien plus élevées, comme on peut le voir avec les points du coin supérieur droit du diagramme de dispersion.
tmpSettings = {'marginal_x' : 'histogram', 'marginal_y' : 'histogram', 'range_x' : [-10e5,20e6],
'color_discrete_sequence' : mycolors, 'opacity' : 0.7}
fig = px.scatter(df_truePred_lrRef_exp, x='true', y='error', range_y=[-17e6, 30e6], **tmpSettings)
fig.update_layout(xaxis_title='True value', yaxis_title='Error : prediction - true value')
Erreur de la prédiction en fonction de la consommation d'énergie du batîment. Plus la consommation en énergie du bâtiment est importante, plus le modèle fait une erreur (absolue) importante sur la prédiction.
Le diagramme de dispersion ci-dessous permet de 'voir' les performances du modèle sous un autre point de vue, avec l'erreur relative (prediction divisée par la valeur vraie) en fonction de la consommation d'énergie. Encore une fois, le point de séparation se trouve vers 1.5M kBtu. En dessous, les consommations sont surestimées, au-dessus, elles sont généralement sous-estimées.
fig = px.scatter(df_truePred_lrRef_exp, x='true', y='relative_error', **tmpSettings)
fig.update_layout(xaxis_title='True value', yaxis_title='Relative error : prediction / true value')
Erreur relative de la prédiction en fonction de la consommation d'énergie du batîment. Plus la consommation en énergie du bâtiment est faible, plus le modèle fait une erreur relative importante sur la prédiction.
## variable cible avec outliers -> log
y_train_log = np.log(1 + y_train)
fig = ff.create_distplot([y_train_log], [''], show_rug=False, bin_size=.25)
fig.update_layout(xaxis_title = 'Log of SiteEnergyUseWN(kBtu)', yaxis_title='Frequency')
fig.show()
Distribution du logarithme de la variable cible avec outliers. Ces derniers ont une consommation d'énergie annuelle nulle.
On cherche dans cette première partie à mesurer l'impact des outliers sur les performances de la régression linéaire. On va supprimer un à un les outliers du jeu d'entraînement et effectuer une régression linéaire à chaque enlevement d'un outlier. Les outliers sont définies comme les batîments de consommation d'énergie annuelle nulle.
Les transformations de données appliquées à cette régression sont :
## standardisation des variables d'entrées
scaler_train = preprocessing.StandardScaler().fit(X_train)
## application du scaler sur le jeu d'entraînement et de test
X_train_normed, X_test_woOut_normed = myfunc.applyScalerTrainTest(scaler_train,
X_train, X_test_woOut)
## à chaque fois qu'on retire un outlier du jeu d'entraînement,
## on effectue une nouvelle régression linéaire
metrics_lrDropOut = []
for index in outliers_train:
posIndexOut = y_train_log.index.get_loc(index) ## position de l'outlier
## supprime 1 outlier de X et y
y_train_log.drop(index, inplace=True)
X_train_normed = np.delete(X_train_normed, posIndexOut, 0)
## prédiction de la régression linéaire avec 1 outlier en moins
y_train_pred_lrDropOut, y_test_pred_lrDropOut = myfunc.linearRegTrainTestPred(
X_train_normed, X_test_woOut_normed, y_train_log)
metrics_lrDropOut.append(myfunc.eval_metric('coucou', y_test_woOut_log, y_test_pred_lrDropOut,
y_train_log, y_train_pred_lrDropOut))
Le graphique ci-dessous indique l'erreur du modèle de regression linéaire (MSE) en fonction du nombre d'outliers rejetés (courbe bleue). On constate une amélioration du modèle avec la suppression des outliers, avec un net gain lorsqu'on supprime tous les outliers (MSE = 0.96 à MSE = 0.73). Lorsque tous les outliers sont rejetés, on retrouve bien les performances de la régression linéaire de référence, indiquée par la courbe rouge.
## conversion en tableau Numpy
metrics_lrDropOut = np.array(metrics_lrDropOut)
## figure MSE vs nb d'outliers supprimés à l'entraînement
fig = go.Figure(go.Scatter(x=list(range(metrics_lrDropOut.shape[0])), y=metrics_lrDropOut[:,1],
mode='lines+markers', name='with outliers' ))
fig.update_layout(xaxis_title='Number of outliers (y = 0.) dropped', yaxis_title='MSE')
## régression linéaire de référence, sans outliers
trace2 = go.Scatter(x=[0,metrics_lrDropOut.shape[0] - 1], y=2 * [metrics_lrRef[1]],
mode='lines', name='baseline')
fig.add_trace(trace2)
fig.show()
Erreur quadratique moyenne (performances) de la régression linéaire en fonction du nombre d'outliers rejetés (bleu). Les performances de la régression linéaire sont très sensibles à la présence des outliers.
Dans cette partie, on étudie les performances de deux régressions linéaires :
Pour ces deux cas, on applique les mêmes transformations suivantes :
On n'oberve pas de différences de performances entre la régression de référence (standardisé) et la régression non standardisée. Les mesures de performances indiquées sur la table ci-dessous sont les égales à 3 chiffres près.
## prédiction de la régression linéaire sans standardisation
y_train_pred_lrNotNormed, y_test_pred_lrNotNormed = myfunc.linearRegTrainTestPred(
X_train_woOut, X_test_woOut, y_train_woOut_log)
## métriques d'évaluation de la reg. linéaire
metrics_lrNotNormed = myfunc.eval_metric('Not Standardized', y_test_woOut_log, y_test_pred_lrNotNormed,
y_train_woOut_log, y_train_pred_lrNotNormed, 1)
table_data = [tableHeader, metrics_lrRef, metrics_lrNotNormed]
fig = ff.create_table(table_data, height_constant=60)
fig.show()
Table des mesures de performances pour la régression linéaire sans standardisation des variables d'entrée.
Une fuite de donnée peut apparaître lorsque l'information du jeu de test est utilisée pendant la modélisation. On s'attend alors à obtenir à obtenir de meilleures performances. Les performances sont surestimées.
Cela peut se produire au niveau de la standardisation des variables d'entrée. En effet, si l'échelonnage (scaler) est ajuster sur tout le jeu de donnée (somme du jeu d'entraînement et du jeu de test), il contient l'information du jeu de test. Une partie de cette information est utilisée par le modèle lorsqu'il est entraîné à partir des données d'entraînements standardisées.
Dans notre cas, on n'oberve pas de différences de performances entre la régression de référence (standardisé) et la régression dont la standardisation présente une fuite de données. Les mesures de performances indiquées sur la table ci-dessous sont les égales à 3 chiffres près.
## tous les outliers
outliers = outliers_train.union(outliers_test)
## variables d'entrées sans outliers
X_woOut = X.drop(outliers)
scaler_woOut = preprocessing.StandardScaler().fit(X_woOut)
## application du scaler sur le jeu d'entraînement et de test
X_train_woOut_DLnormed, X_test_woOut_DLnormed = myfunc.applyScalerTrainTest(scaler_woOut,
X_train_woOut, X_test_woOut)
## prédiction de la régression linéaire
y_train_pred_lrDLeak, y_test_pred_lrDLeak = myfunc.linearRegTrainTestPred(
X_train_woOut_DLnormed, X_test_woOut_DLnormed, y_train_woOut_log)
metrics_lrLogDLeak = myfunc.eval_metric('Data Leak Standardization', y_test_woOut_log, y_test_pred_lrDLeak,
y_train_woOut_log, y_train_pred_lrDLeak)
# Add table data
table_data = [tableHeader, metrics_lrRef, metrics_lrLogDLeak]
fig = ff.create_table(table_data, height_constant=60)
fig.show()
Table des mesures de performances pour la régression linéaire dont la standardisation des variables d'entrée présente une fuite de données.
fig3 = go.Figure()
bining = dict( # bins used for histogram
start=-3.0,
end=3,
size=0.1)
fig3.add_traces(go.Histogram(x =X_test_woOut_DLnormed[:,-1], xbins=bining, name='Data leak'))
fig3.add_traces(go.Histogram(x =X_test_woOut_normed[:,-1], xbins=bining, name='Standard'))
fig3.update_layout(xaxis_title = 'LargestPropertyUseTypeGFA (Standard Scaled)',barmode='overlay', width=480)
fig3.update_traces(opacity=0.60)
fig3.show()
Distribution standardisée de la variable d'entrée LargestPropertyUseTypeGFA, avec (courbe bleue) et sans (courbe rouge) fuite de données.
On est en présence d'un phénomène approximativement multiplicatif : les variations de la consommation d'énergie d'un batîment sont d'autant plus grandes que sa consommation d'énergie est importante. Dans ce cas de figure, il est préférable de choisir un modèle qui pénalise les prédictions selon l'erreur relative plutôt que l'erreur absolue. Cela revient à utiliser comme objectif la MSLE (erreur logarithmique au carré moyenne) plutôt que le MSE (erreur quadratique moyenne).
Hors l'implémentation de la régression linéaire de Scikit-learn utilise seulement la MSE comme objectif. Passer la variable cible au logarithme permet de transformer le problème de minimisation du MSE en une minimisation du MSLE. On étudie le cas d'une régression sans passage de la variable cible au log, que l'on compare avec la régression de référence. Cela revient à comparer le MSE et le MSLE comme objectif d'une régression.
## entraînement de la régression linéaire
lrNoLog = linear_model.LinearRegression()
lrNoLog.fit(X_train_woOut_normed, y_train_woOut)
y_test_pred_lrNoLog = lrNoLog.predict(X_test_woOut_normed) ## prédiction sur le jeu test
y_train_pred_lrNoLog = lrNoLog.predict(X_train_woOut_normed) ## '' sur le jeu entraînement
## prédiction de la régression linéaire
y_train_pred_lrNoLog, y_test_pred_lrNoLog = myfunc.linearRegTrainTestPred(
X_train_woOut_normed, X_test_woOut_normed, y_train_woOut)
metrics_lrNoLog = myfunc.eval_metric('Not trained on log(y)', y_test_woOut, y_test_pred_lrNoLog,
y_train_woOut, y_train_pred_lrNoLog, scientific = 1)
## dataframe avec prédiction / valeur vraie de y
df_truePred_lrNoLog = myfunc.yTruePred(y_test_woOut, y_test_pred_lrNoLog)
La distribution de la consommation d'énergie prédite est différente pour un entraînement sur le logarithme de la variable cible (graphe de gauche ci-dessous) et pour un entraînement sur la variable cible elle-même (graphe de droite ci-dessous). Le MSE est sensible aux fortes différences entre la prédiction et la valeur vraie, que le second modèle tente de minimiser. Hors, ces différences sont d'autant plus fortes que les consommations d'énergie sont élevées. En bref, le second modèle donne plus de poids aux batîments à forte consommation d'énergie. Il en résulte des prédictions d'énergie plus élevées.
fig4 = px.histogram(df_truePred_lrNoLog, x=['true', 'pred'], title='training w/o log of target',
range_x = [0, 20e6], nbins = 500, barmode='overlay')
fig4.update_layout(xaxis_title='SiteEnergyUseWN(kBtu)', width= 480)
fig2.update_layout(title='training with log of target', )
ipy.HBox([go.FigureWidget(fig2), go.FigureWidget(fig4)])
Distribution de la consommation d'énergie annuelle, pour une modélisation sur le logarithme de la variable cible (à gauche), et pour une modélisation sur la variable cible elle-même (à droite). La distribution de la valeur vraie (bleue) est comparée à la valeur prédite par le modèle (rouge).
## ajout au dataframe du type de regression
df_truePred_lrNoLog['regression'] = ['train on y'] * len(df_truePred_lrNoLog)
df_truePred_lrRef_exp['regression'] = ['train on log y'] * len(df_truePred_lrRef_exp)
## concaténation des deux dataframes
df_truePred_lrRef_lrNoLog = pd.concat([df_truePred_lrRef_exp, df_truePred_lrNoLog])
Concernant l'évolution de l'erreur en fonction de la consommation (voir graphe ci-dessous), les deux modèles présentent des evolutions similaires, c'est-à -dire une sous-estimation d'autant plus prononcée que la consommation d'énergie est élevée. Le modèle MSE (entraîné sur y) 'contient' les valeurs dans un intervalle plus réduit. En effet, le modèle MSLE (entraîné sur log(y)) autorise des erreurs plus importantes notamment à grande consommation d'énergie, car l'erreur relative n'explose pas.
#tmpSettings = {'marginal_x' : 'histogram', 'marginal_y' : 'histogram', 'range_x' : [-10e5,20e6]}
fig = px.scatter(df_truePred_lrRef_lrNoLog, x='true', y='error', color='regression', opacity=0.65,
range_y=[-9e6, 9e6], range_x = [-3e5,9e6], marginal_y='histogram',
color_discrete_sequence=mycolors
)
fig.update_layout(xaxis_title='True value', yaxis_title='Error : prediction - true value')
fig.show()
Erreur de la prédiction en fonction de la consommation d'énergie du batîment, pour une modélisation sur le logarithme de la variable cible (à gauche), et pour une modélisation sur la variable cible elle-même (à droite).
La figure ci-dessous présente la dispersion des erreurs relatives en fonction de la consommation d'énergie. L'erreur relative est plus 'contenue' pour un apprentissage sur le logarithme de la variable cible (MSLE), i.e les points oranges s'approches beaucoup plus de 1 que la courbe y.
fig = px.scatter(df_truePred_lrRef_lrNoLog, x='true', y='relative_error', color='regression', opacity=0.65,
marginal_y='histogram', range_y=[-1, 13], range_x = [-3e5,9e6], color_discrete_sequence=mycolors
)
fig.show()
Erreur relative de la prédiction en fonction de la consommation d'énergie du batîment, pour une modélisation sur le logarithme de la variable cible (à gauche), et pour une modélisation sur la variable cible elle-même (à droite).
## on retire toutes les consommations en-dessous de 50e6
df_truePred_lrRef_exp_sel = df_truePred_lrRef_exp[df_truePred_lrRef_exp['true'] < 30e6]
## régression de réf., on repasse la variable cible à la normale par l'exp
metrics_lrRefExp = myfunc.eval_metric('Baseline Exponential', df_truePred_lrRef_exp_sel['true'],
df_truePred_lrRef_exp_sel['pred'],
np.exp(y_train_woOut_log)-1,
np.exp(y_train_pred_lrRef)-1, numpy=1, scientific=1)
# Add table data
table_data = [tableHeader, metrics_lrRefExp, metrics_lrNoLog]
fig = ff.create_table(table_data, height_constant=60)
fig.show()
Table des mesures de performances pour la régression linéaire dont la standardisation des variables d'entrée présente une fuite de données.
On a réalisé une modélisation linéaire de la consommation d'énergie, et étudié l'impact de trois transformations de données sur les performance du modèles. Les résultats obersés sont les suivants :
On observe une forte sensibilité de la régression linéaire avec la présence d'outliers, ici des bâtiments aux consommation d'énergie nulle. Ces outliers réduise le pouvoir prédictif du modèle.
Aucune amélioration n'a été observée, à trois chiffres près, due à la standardisation des variables d'entrée pour un modèle de régression linéaire.
La modélisation sans le passage au log de la variable cible à tendance à overfitter les bâtiments aux très hautes énergie, au détriments de l'ensemble des bâtiments ayant une énergie proche de la moyenne/médiane.